import path from 'path';
import { indent } from '../tools/vendor.js';
import { EntryDef, ProjectCfg, StructDef } from '../typings/index.js';
import { BaseStructParser } from './BaseStructParser.js';
import { BigintLike, StringLike } from './def.js';

export class ProtocolStructParser extends BaseStructParser {

    private static ins: ProtocolStructParser;
    public static get Instance(): ProtocolStructParser {
        if(!ProtocolStructParser.ins) ProtocolStructParser.ins = new ProtocolStructParser();
        return ProtocolStructParser.ins;
    }

    private readonly encodeOpMap: { [type: string]: string } = {
        "byte": "writeByte",
        "char": "writeByte",			
        "Double": "writeFloat32",
        "Float": "writeFloat32",
        "float": "writeFloat32",
        "Int32": "writeInt32",
        "Uint32": "writeUint32",
        "Int16": "writeInt16",
        "Uint16": "writeUint16",
        "Int8": "writeByte",
        "Uint8": "writeUint8",
        "uchar": "writeByte",
        "ushort": "writeUint16",
        "short": "writeInt16",
        "int": "writeInt32",
        "uint": "writeUint32",
        "Smallint": "writeInt16",
        "smallint": "writeInt16",
        "Smalluint": "writeUint16",
        "smalluint": "writeUint16",
        "Tinyuint": "writeUint8",
        "Tinyint": "writeUint8"
    };

    private readonly decodeOpMap: { [type: string]: string } = {
        "byte": "getUint8",
        "char": "getByte",			
        "Double": "getFloat32",
        "Float": "getFloat32",
        "float": "getFloat32",
        "Int32": "getInt32",
        "Uint32": "getUint32",
        "Int16": "getInt16",
        "Uint16": "getUint16",
        "Int8": "getByte",
        "Uint8": "getUint8",
        "uchar": "getUint8",
        "ushort": "getUint16",
        "short": "getInt16",
        "int": "getInt32",
        "uint": "getUint32",
        "Smallint": "getInt16",
        "smallint": "getInt16",
        "Smalluint": "getUint16",
        "smalluint": "getUint16",
        "Tinyuint": "getUint8",
        "Tinyint": "getByte"
    };

    private readonly sizeMap: { [type: string]: 1 | 2 | 4 } = {
        "byte": 1,
        "char": 1,			
        "Double": 4,
        "Float": 4,
        "float": 4,
        "Int32": 4,
        "Uint32": 4,
        "Int16": 2,
        "Uint16": 2,
        "Int8": 1,
        "Uint8": 1,
        "uchar": 1,
        "ushort": 2,
        "short": 2,
        "int": 4,
        "uint": 4,
        "Smallint": 2,
        "smallint": 2,
        "Smalluint": 2,
        "smalluint": 2,
        "Tinyuint": 1,
        "Tinyint": 1
    };

    protected get rmFlashTail(): boolean {
        return false;
    }

    public async readStructs(root: string, projCfg: ProjectCfg): Promise<string[]> {
        this.reset();
        const XmlNames = projCfg.Project.ProjectType === 'H5ts' ? ['CommonCS.xml', 'ProtoTypeCS.xml', 'CS.xml'] : ['Common.xml', 'ProtoType.xml', 'CS.xml'];
        for(const n of XmlNames) {
            const f = path.join(root, n)

            await this.readStructFile(f, false);
        }

        return this.m_cliStructs.sort();
    }

    protected isClientStruct(struct: StructDef): boolean {
        return struct._attributes.server === undefined;
    }

    public makeStructDecodeCode(structName: string): string {
        const wrap = this.m_structDefMap[structName];
        if(!wrap) {
            console.error('[ERROR] makeStructDecodeCode: No struct definition!', structName);
            process.exit(1);
        }
        const def = wrap.def;
        let code: string, body: string;
        if (def._attributes.class === 'union') {
            code = `private _decode${structName}(select: number, body: Protocol.${structName}, byte: Byte): Protocol.${structName} {\n`;
            body = '    switch(select) {\n';
            for(const e of def.entries) {
                const macro = this.getMacro(e._attributes.value!);
                body += `    case ${macro?._attributes.value}: //${e._attributes.value}\n`;
                body += '    {\n';
                body += indent(this.makeEntryDecodeCode(e), '        ');
                body += '        break;\n';
                body += '    }\n';
            }
            body += '    }\n';
        } else {
            code = `private _decode${structName}(body: Protocol.${structName}, byte: Byte): Protocol.${structName} {\n`;
            body = '';
            for(const e of def.entries) {
                body += indent(this.makeEntryDecodeCode(e));
            }
        }
        code += `    if (!body) body = {} as Protocol.${structName};\n`;
        code += body;
        code += `    return body;
}\n\n`;
        return code;
    }

    private makeEntryDecodeCode(e: EntryDef): string {
        let fieldName = `body.${e._attributes.name}`;
        if (e._attributes.count) {
            fieldName += '[i]';
        }
        const op = this.decodeOpMap[e._attributes.type];
        let body: string;
        if (op) {
            body = `${fieldName} = byte.${op}();`;
        } else if (BigintLike.includes(e._attributes.type)) {
            body = `${fieldName} = DrUtil.readLonglong(byte);`;
        } else if (StringLike.includes(e._attributes.type)) {
            let opStr = `byte.getUTFBytes(__${e._attributes.name}Size - 1)`;
            if (e._attributes.safeprintf) {
                opStr = `this.__safeprintf(${opStr})`;
            }
            body = `const __${e._attributes.name}Size = byte.getUint32();
${fieldName} = ${opStr};
byte.getByte();`;
        } else {
            if (e._attributes.select) {
                body = `${fieldName} = this._decode${e._attributes.type}(body.${e._attributes.select}, null, byte);`;
            } else {
                body = `${fieldName} = this._decode${e._attributes.type}(null, byte);`;
            }
        }

        let out: string;
        if (e._attributes.count) {
            // 数组
            let numdef: string;
            if (e._attributes.refer) {
                numdef = `body.${e._attributes.refer}`;
            } else {
                numdef = `Macros.${e._attributes.count}`;
            }

            out = `if (!body.${e._attributes.name}) body.${e._attributes.name} = [];
else body.${e._attributes.name}.length = ${numdef};
for (let i = 0, num = ${numdef}; i < num; i++) {
${indent(body)}
}`;
        } else {
            out = body;
        }
        return out + '\n';
    }

    public makeStructEncodeCode(structName: string): string {
        const wrap = this.m_structDefMap[structName];
        if(!wrap) {
            console.error('[ERROR] makeStructDecodeCode: No struct definition!', structName);
            process.exit(1);
        }
        const def = wrap.def;
        let code: string, body: string;
        if (def._attributes.class === 'union') {
            code = `private _encode${structName}(select: number, body: Protocol.${structName}, byte: Byte): void {\n`;
            body = '    switch(select) {\n';
            for(const e of def.entries) {
                const macro = this.getMacro(e._attributes.value!);
                body += `    case ${macro?._attributes.value}: //${e._attributes.value}\n`;
                body += '    {\n';
                body += indent(this.makeEntryEncodeCode(structName, e), '        ');
                body += '        break;\n';
                body += '    }\n';
            }
            body += '    }\n';
        } else {
            code = `private _encode${structName}(body: Protocol.${structName}, byte: Byte): void {\n`;
            body = '';
            for(const e of def.entries) {
                body += indent(this.makeEntryEncodeCode(structName, e));
            }
        }
        code += body;
        code += '}\n\n';
        return code;
    }

    private makeEntryEncodeCode(structName: string, e: EntryDef): string {
        let fieldName = `body.${e._attributes.name}`;
        if (e._attributes.count) {
            fieldName += '[i]';
        }
        let body: string;
        const op = this.encodeOpMap[e._attributes.type];
        if (op) {
            body = `byte.${op}(${fieldName});\n`;
        } else if (BigintLike.includes(e._attributes.type)) {
            body = `DrUtil.writeLonglong(byte, ${fieldName});\n`;
        } else if (StringLike.includes(e._attributes.type)) {
            body = `const __${e._attributes.name}Size = byte.pos;
byte.pos += 4;
const __${e._attributes.name}Begin = byte.pos;
byte.writeUTFBytes(${fieldName});
byte.writeByte(0);
DrUtil.directWriteUint(byte, __${e._attributes.name}Size, byte.pos - __${e._attributes.name}Begin);\n`;
        } else {
            if (e._attributes.select) {
                body = `this._encode${e._attributes.type}(body.${e._attributes.select}, ${fieldName}, byte);\n`;
            } else {
                body = `this._encode${e._attributes.type}(${fieldName}, byte);\n`;
            }
        }
        let out: string;
        if (e._attributes.count) {
            // 数组
            let numdef: string;
            if (e._attributes.refer) {
                numdef = `body.${e._attributes.refer}`;
            } else {
                numdef = `Macros.${e._attributes.count}`;
            }
            out = `for (let i = 0, num = ${numdef}; i < num; i++) {\n`;
            out += indent(body);
            out += '}\n';
        } else {
            out = body;
        }
        return out;
    }
}